import os
import re
import queue
import time
import threading
import webbrowser
import requests
import tkinter as tk
from tkinter import messagebox, filedialog, ttk
from tkinterdnd2 import TkinterDnD, DND_FILES
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
import shutil

# Путь к файлу с токенами
TOKENS_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\токены для автопостинга.txt"
# Путь к файлу для неуспешных публикаций
FAILED_TOKENS_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\Токены неуспешных публикаций.txt"
# Путь к файлу для хранения последнего выложенного поста
LAST_POST_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\last_post.txt"
# Путь к папке с постами
POSTS_DIR = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\посты"
# Путь к папке для загруженных постов
UPLOADED_POSTS_DIR = os.path.join(POSTS_DIR, "загруженные")

# Словарь для хранения информации о пользователях
user_info_dict = {}

# Настройки для более приятного дизайна
PRIMARY_COLOR = "#4CAF50"       # Основной цвет (зелёный)
SECONDARY_COLOR = "#f0f0f0"     # Цвет фона приложения
BUTTON_COLOR = "#2196F3"        # Цвет кнопок (синий)
FONT_STYLE = ("Arial", 12)
HEADER_FONT = ("Arial", 14, "bold")
LINK_COLOR = "#0b7dda"          # Цвет ссылок

# Очередь для сообщений из потоков
log_queue = queue.Queue()

# Флаг для остановки процесса публикации
stop_flag = threading.Event()

# Счётчик для уникальных тегов гиперссылок
hyperlink_counter = 0

# Функция для сортировки с учётом чисел в названиях файлов
def natural_key(filename):
    basename = os.path.basename(filename)
    return [text for text in re.split(r'(\d+)', basename)]

def get_user_info(token):
    url = 'https://api.vk.com/method/users.get'
    params = {
        'access_token': token,
        'v': '5.131'
    }
    response = requests.get(url, params=params)
    data = response.json()
    if 'response' in data:
        user_info = data['response'][0]
        user_info_dict[token] = f"{user_info['first_name']} {user_info['last_name']}"
        return user_info['id'], user_info_dict[token]
    elif 'error' in data and data['error'].get('error_code') == 5:
        raise Exception(f"Ошибка авторизации: {data['error']['error_msg']}")
    else:
        raise Exception("Ошибка получения информации о пользователе: " + str(data.get('error', {})))

def upload_photo(token, photo_path):
    url = 'https://api.vk.com/method/photos.getWallUploadServer'
    params = {
        'access_token': token,
        'v': '5.131'
    }
    response = requests.post(url, params=params)
    response.raise_for_status()
    upload_info = response.json()

    if 'response' not in upload_info:
        raise Exception("Ошибка получения сервера для загрузки фото: " + str(upload_info.get('error', {})))

    upload_url = upload_info['response']['upload_url']
    
    with open(photo_path, 'rb') as photo_file:
        files = {'photo': photo_file}
        upload_response = requests.post(upload_url, files=files)
        upload_response.raise_for_status()
        upload_data = upload_response.json()

    if 'photo' not in upload_data:
        raise Exception("Ошибка загрузки фото: " + str(upload_data))

    return upload_data

def save_photo(token, upload_data, user_id):
    url = 'https://api.vk.com/method/photos.saveWallPhoto'
    params = {
        'access_token': token,
        'user_id': user_id,
        'server': upload_data['server'],
        'photo': upload_data['photo'],
        'hash': upload_data['hash'],
        'v': '5.131'
    }
    response = requests.post(url, params=params)
    response.raise_for_status()
    data = response.json()
    if 'response' not in data:
        raise Exception("Ошибка сохранения фото: " + str(data.get('error', {})))
    return data

def upload_video(token, video_path):
    url = 'https://api.vk.com/method/video.save'
    params = {
        'access_token': token,
        'v': '5.131',
        'name': os.path.basename(video_path)
    }
    response = requests.post(url, params=params)
    response.raise_for_status()
    video_info = response.json()

    if 'response' not in video_info:
        raise Exception("Ошибка получения сервера для загрузки видео: " + str(video_info.get('error', {})))

    upload_url = video_info['response']['upload_url']
    
    with open(video_path, 'rb') as video_file:
        files = {'video_file': video_file}
        upload_response = requests.post(upload_url, files=files)
        upload_response.raise_for_status()
        upload_data = upload_response.json()

    if 'video_id' not in upload_data:
        raise Exception("Ошибка загрузки видео: " + str(upload_data))

    return upload_data

def post_to_vk(token, user_id, message, attachments):
    url = 'https://api.vk.com/method/wall.post'
    params = {
        'access_token': token,
        'owner_id': user_id,
        'message': message,
        'attachments': attachments,
        'v': '5.131'
    }
    response = requests.post(url, params=params)
    response.raise_for_status()
    data = response.json()
    if 'error' in data:
        raise Exception("Ошибка публикации поста: " + data['error']['error_msg'])
    return data

def submit_post():
    stop_flag.clear()  
    post_button['state'] = tk.DISABLED
    stop_button['state'] = tk.NORMAL
    if autopost_var.get():
        try:
            post_limit = int(post_limit_var.get())
            photo_count = int(photo_count_var.get())
            if post_limit <= 0 or photo_count <= 0:
                messagebox.showerror("Ошибка", "Лимит постов и количество фотографий должны быть больше 0.")
                post_button['state'] = tk.NORMAL
                stop_button['state'] = tk.DISABLED
                return
            thread = threading.Thread(target=post_to_accounts_concurrently, args=(post_limit, photo_count))
        except ValueError:
            messagebox.showerror("Ошибка", "Введите корректные числовые значения для лимита постов и количества фотографий.")
            post_button['state'] = tk.NORMAL
            stop_button['state'] = tk.DISABLED
    else:
        thread = threading.Thread(target=post_to_accounts_concurrently)
    thread.start()

def stop_posting():
    stop_flag.set()
    log_queue.put("Публикация остановлена пользователем.\n")
    stop_button['state'] = tk.DISABLED
    post_button['state'] = tk.NORMAL
    # Восстанавливаем состояние кнопки "Добавить набор публикаций" в зависимости от автопубликации
    add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL

def update_add_schedule_button_state(*args):
    """Обновляет состояние кнопки 'Добавить набор публикаций' в зависимости от автопубликации."""
    add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL

def post_to_account(token, user_id, message, photos, videos):
    if stop_flag.is_set():
        return ("Публикация остановлена пользователем.", False, None)
    max_attempts = 10
    attempt = 0
    user_name = ""
    while attempt < max_attempts:
        if stop_flag.is_set():
            return ("Публикация остановлена пользователем.", False, None)
        attempt += 1
        try:
            user_id, user_name = get_user_info(token)
            log_queue.put(f"Публикация на аккаунте: {user_name}... Попытка {attempt}\n")
            attachments = []

            for photo in photos:
                if stop_flag.is_set():
                    return ("Публикация остановлена пользователем.", False, None)
                log_queue.put(f"Загрузка фото: {os.path.basename(photo)} на аккаунт {user_name}...\n")
                upload_data = upload_photo(token, photo)
                saved_photo = save_photo(token, upload_data, user_id)
                if 'response' in saved_photo:
                    attachments.append(f"photo{saved_photo['response'][0]['owner_id']}_{saved_photo['response'][0]['id']}")
                else:
                    raise Exception("Не удалось сохранить фото.")

            for video in videos:
                if stop_flag.is_set():
                    return ("Публикация остановлена пользователем.", False, None)
                log_queue.put(f"Загрузка видео: {os.path.basename(video)} на аккаунт {user_name}...\n")
                upload_data = upload_video(token, video)
                if 'video_id' in upload_data:
                    attachments.append(f"video{user_id}_{upload_data['video_id']}")
                else:
                    raise Exception("Не удалось загрузить видео.")

            result = post_to_vk(token, user_id, message, ','.join(attachments))
            post_id = result['response']['post_id']
            link = f"https://vk.com/wall{user_id}_{post_id}"
            return (f"Пост успешно опубликован на аккаунте: {user_name}", link, user_id)
        except Exception as e:
            if attempt >= max_attempts:
                user_name = user_info_dict.get(token, token)
                error_message = f"Ошибка на аккаунте: {user_name}: {str(e)} после {max_attempts} попыток\n"
                return error_message, False, token
            else:
                log_queue.put(f"Попытка {attempt} не удалась на аккаунте {user_name}. Ошибка: {e}\n")
                time.sleep(1)

def move_uploaded_files(files):
    os.makedirs(UPLOADED_POSTS_DIR, exist_ok=True)
    for file in files:
        try:
            shutil.move(file, os.path.join(UPLOADED_POSTS_DIR, os.path.basename(file)))
            log_queue.put(f"Файл перемещён: {os.path.basename(file)}\n")
        except Exception as e:
            log_queue.put(f"Ошибка при перемещении файла {os.path.basename(file)}: {e}\n")

def post_to_accounts_concurrently(post_limit=None, photo_count=None):
    global successful_posts_list, errors_list
    successful_posts_list = []
    errors_list = []
    failed_tokens_set = set()  
    accounts_posted_set = set()  

    if autopost_var.get() and post_limit and photo_count:
        if not os.path.exists(POSTS_DIR):
            log_queue.put(f"Папка {POSTS_DIR} не найдена.\n")
            post_button['state'] = tk.NORMAL
            stop_button['state'] = tk.DISABLED
            add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL
            return
        all_photos = sorted([
            os.path.join(POSTS_DIR, f) for f in os.listdir(POSTS_DIR)
            if f.lower().endswith(('.jpg', '.jpeg', '.png'))
        ], key=natural_key)
        if not all_photos:
            log_queue.put("В папке 'посты' нет фотографий.\n")
            post_button['state'] = tk.NORMAL
            stop_button['state'] = tk.DISABLED
            add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL
            return
        posting_schedule.clear()
        update_posting_schedule_table()
        for i in range(0, min(post_limit * photo_count, len(all_photos)), photo_count):
            photos = all_photos[i:i + photo_count]
            posting_schedule.append({'text': '', 'photos': photos, 'videos': []})
        log_queue.put(f"Сформировано {len(posting_schedule)} постов для автопубликации.\n")
        update_posting_schedule_table()

    total_tokens = len(tokens)
    total_accounts = len(posting_schedule) * len(tokens)
    progress_bar['maximum'] = total_accounts
    progress_bar['value'] = 0

    output_text.delete("1.0", tk.END)
    successful_text.delete("1.0", tk.END)
    failed_text.delete("1.0", tk.END)

    successful_posts = 0
    failed_posts = 0

    for entry in posting_schedule:
        entry['photos'] = sorted(entry['photos'], key=natural_key)
        entry['videos'] = sorted(entry['videos'], key=natural_key)

    log_queue.put("Файлы отсортированы.\n")

    for entry_idx, entry in enumerate(posting_schedule):
        if stop_flag.is_set():
            break
        message = entry['text']
        photos = entry['photos']
        videos = entry['videos']
        successful_tokens = 0
        with ThreadPoolExecutor(max_workers=10) as executor:
            valid_tokens = [token for token in tokens if token not in failed_tokens_set]
            future_to_token = {
                executor.submit(post_to_account, token, None, message, photos, videos): token 
                for token in valid_tokens
            }
            for future in as_completed(future_to_token):
                if stop_flag.is_set():
                    break
                token = future_to_token[future]
                try:
                    result = future.result()
                    if len(result) == 3:
                        if result[1] == False:
                            failed_posts += 1
                            update_failed_posts_threadsafe(result[0])
                            failed_tokens_set.add(result[2])
                        else:
                            successful_posts += 1
                            successful_tokens += 1
                            accounts_posted_set.add(result[2])
                            update_successful_posts_threadsafe(result[0], result[1])
                    else:
                        failed_posts += 1
                        update_failed_posts_threadsafe("Неизвестная ошибка.\n")
                        failed_tokens_set.add(token)
                    progress_bar['value'] += 1
                    update_progress_label_threadsafe(successful_posts, failed_posts, total_accounts)
                except Exception as exc:
                    user_name = user_info_dict.get(token, token)
                    error_message = f"Ошибка на аккаунте: {user_name}: {str(exc)}\n"
                    failed_posts += 1
                    update_failed_posts_threadsafe(error_message)
                    failed_tokens_set.add(token)
                    progress_bar['value'] += 1
                    update_progress_label_threadsafe(successful_posts, failed_posts, total_accounts)
        # Перемещаем файлы только после успешной публикации на всех токенах, если автопубликация включена
        if autopost_var.get() and successful_tokens == len(valid_tokens) and not stop_flag.is_set():
            move_uploaded_files(photos + videos)

    if failed_tokens_set:
        write_failed_tokens(failed_tokens_set)

    log_queue.put("Публикация завершена.\n")
    stop_button['state'] = tk.DISABLED
    post_button['state'] = tk.NORMAL
    # Восстанавливаем состояние кнопки "Добавить набор публикаций" после завершения публикации
    add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL

def write_failed_tokens(failed_tokens_set):
    separator = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " ------------------------\n"
    try:
        with open(FAILED_TOKENS_FILE_PATH, 'a', encoding='utf-8') as file:
            file.write(separator)
            for token in failed_tokens_set:
                file.write(token + '\n')
        log_queue.put(f"Токены неуспешных публикаций записаны в файл {FAILED_TOKENS_FILE_PATH}\n")
    except Exception as e:
        log_queue.put(f"Ошибка при записи токенов неуспешных публикаций: {e}\n")

def update_progress_label_threadsafe(success_count, failed_count, total_accounts):
    remaining_count = total_accounts - (success_count + failed_count)
    progress_info = {
        'success_count': success_count,
        'failed_count': failed_count,
        'remaining_count': remaining_count
    }
    log_queue.put(('update_progress', progress_info))

def update_successful_posts_threadsafe(message, link):
    log_queue.put(('update_success', message, link))

def update_failed_posts_threadsafe(message):
    log_queue.put(('update_failed', message))

def process_queue():
    try:
        while True:
            item = log_queue.get_nowait()
            if isinstance(item, str):
                output_text.insert(tk.END, item)
                if auto_scroll_var.get():
                    output_text.see(tk.END)
            elif isinstance(item, tuple):
                if item[0] == 'update_progress':
                    info = item[1]
                    progress_label.config(text=f"Осталось постов: {info['remaining_count']}")
                    successful_posts_label.config(text=f"Успешные публикации: {info['success_count']}")
                    failed_posts_label.config(text=f"Неуспешные публикации: {info['failed_count']}")
                elif item[0] == 'update_success':
                    message, link = item[1], item[2]
                    update_successful_posts(message, link)
                elif item[0] == 'update_failed':
                    message = item[1]
                    update_failed_posts(message)
    except queue.Empty:
        pass
    root.after(100, process_queue)

def update_successful_posts(message, link):
    global hyperlink_counter
    # Создаём уникальный тег для гиперссылки
    tag_name = f"hyperlink_{hyperlink_counter}"
    hyperlink_counter += 1
    successful_text.insert(tk.END, message + " ")
    successful_text.insert(tk.END, link, tag_name)
    successful_text.tag_config(tag_name, foreground=LINK_COLOR, underline=True)
    successful_text.tag_bind(tag_name, "<Button-1>", lambda e: webbrowser.open(link))
    successful_text.insert(tk.END, "\n")
    if auto_scroll_var.get():
        successful_text.see(tk.END)

def update_failed_posts(message):
    failed_text.insert(tk.END, message + "\n")
    if auto_scroll_var.get():
        failed_text.see(tk.END)

def load_tokens():
    global tokens
    if os.path.exists(TOKENS_FILE_PATH):
        with open(TOKENS_FILE_PATH, 'r') as file:
            tokens = [token.strip() for token in file.readlines() if token.strip()]
        tokens_label.config(text=f"Токенов: {len(tokens)}")
        if tokens:
            enable_buttons()
            autopost_check['state'] = tk.NORMAL  # Активируем галочку, если токены загружены
        else:
            messagebox.showwarning("Предупреждение", "Файл не содержит токенов.")
    else:
        messagebox.showerror("Ошибка", f"Файл с токенами не найден по пути: {TOKENS_FILE_PATH}")

def enable_buttons():
    post_button['state'] = tk.NORMAL
    add_schedule_button['state'] = tk.DISABLED if autopost_var.get() else tk.NORMAL
    remove_schedule_button['state'] = tk.NORMAL

def create_context_menu(widget):
    context_menu = tk.Menu(widget, tearoff=0, font=FONT_STYLE, bg="white", fg="black")
    context_menu.add_command(label="Вырезать", command=lambda: widget.event_generate("<<Cut>>"))
    context_menu.add_command(label="Копировать", command=lambda: widget.event_generate("<<Copy>>"))
    context_menu.add_command(label="Вставить", command=lambda: widget.event_generate("<<Paste>>"))
    context_menu.add_separator()
    context_menu.add_command(label="Выбрать все", command=lambda: widget.event_generate("<<SelectAll>>"))
    def toggle_auto_scroll():
        auto_scroll_var.set(not auto_scroll_var.get())
    if auto_scroll_var.get():
        auto_scroll_label = "Отключить автопрокрутку"
    else:
        auto_scroll_label = "Включить автопрокрутку"
    context_menu.add_separator()
    context_menu.add_command(label=auto_scroll_label, command=toggle_auto_scroll)
    return context_menu

def show_context_menu(event, widget):
    context_menu = create_context_menu(widget)
    context_menu.tk_popup(event.x_root, event.y_root)

def add_posting_set():
    add_window = tk.Toplevel(root)
    add_window.title("Добавить набор публикации")
    add_window.geometry("700x500")
    add_window.configure(bg=SECONDARY_COLOR)
    add_window.attributes("-topmost", True)

    title_label = ttk.Label(add_window, text="Новый набор публикации", font=HEADER_FONT)
    title_label.grid(row=0, column=0, columnspan=3, pady=(10, 5), padx=10, sticky="w")

    files_label_text = ttk.Label(add_window, text="Файлы (Фото и Видео):")
    files_label_text.grid(row=1, column=0, padx=10, pady=5, sticky="w")

    files_listbox = tk.Listbox(add_window, selectmode=tk.MULTIPLE, width=40, height=10)
    files_listbox.grid(row=2, column=0, padx=10, pady=5, sticky="w")

    files_buttons_frame = ttk.Frame(add_window)
    files_buttons_frame.grid(row=2, column=1, padx=5, pady=5, sticky="w")

    posting_schedule_files = []

    def add_files_drop_event(event, file_list, listbox):
        files = add_window.tk.splitlist(event.data)
        valid_files = [f for f in files 
                       if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.mp4', '.avi', '.mov', '.mkv'))]
        for file in valid_files:
            if file not in file_list:
                file_list.append(file)
                listbox.insert(tk.END, os.path.basename(file))
        log_queue.put(f"Добавлено файлов через DnD: {[os.path.basename(file) for file in valid_files]}\n")

    files_listbox.drop_target_register(DND_FILES)
    files_listbox.dnd_bind('<<Drop>>', lambda event: add_files_drop_event(event, posting_schedule_files, files_listbox))

    def add_files():
        files = filedialog.askopenfilenames(
            title="Выберите фото и/или видео",
            filetypes=[("Image and Video files", "*.jpg;*.jpeg;*.png;*.gif;*.mp4;*.avi;*.mov;*.mkv")]
        )
        for file in files:
            if file not in posting_schedule_files:
                posting_schedule_files.append(file)
                files_listbox.insert(tk.END, os.path.basename(file))
        log_queue.put(f"Добавлено файлов через диалог: {[os.path.basename(file) for file in files]}\n")

    add_files_button = ttk.Button(files_buttons_frame, text="+", width=3, command=add_files)
    add_files_button.pack(side=tk.LEFT, padx=(0, 2))

    def remove_selected_files():
        selected_indices = list(files_listbox.curselection())
        selected_indices.sort(reverse=True)
        for index in selected_indices:
            files_listbox.delete(index)
            del posting_schedule_files[index]
        log_queue.put("Удалены выбранные файлы.\n")

    remove_files_button = ttk.Button(files_buttons_frame, text="-", width=3, command=remove_selected_files)
    remove_files_button.pack(side=tk.LEFT)

    text_label = ttk.Label(add_window, text="Текст сообщения:")
    text_label.grid(row=3, column=0, padx=10, pady=(10, 5), sticky="w")

    message_entry = tk.Text(add_window, height=5, width=50, font=FONT_STYLE, wrap="word")
    message_entry.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="w")

    message_entry.bind("<Button-3>", lambda event: show_context_menu(event, message_entry))

    def paste_message_in_child(entry_widget):
        try:
            clipboard_text = add_window.clipboard_get()
            entry_widget.delete("1.0", tk.END)
            entry_widget.insert(tk.END, clipboard_text)
        except tk.TclError:
            messagebox.showerror("Ошибка", 
                                 "Буфер обмена пуст или содержит неподдерживаемые данные.", 
                                 parent=add_window)

    paste_button_child = ttk.Button(add_window, text="Вставить", 
                                    command=lambda: paste_message_in_child(message_entry))
    paste_button_child.grid(row=4, column=2, padx=5, pady=5, sticky="w")

    message_scrollbar = ttk.Scrollbar(add_window, orient="vertical", command=message_entry.yview)
    message_scrollbar.grid(row=4, column=3, sticky='nsw', padx=(0,10), pady=5)
    message_entry.config(yscrollcommand=message_scrollbar.set)

    buttons_frame = ttk.Frame(add_window)
    buttons_frame.grid(row=5, column=0, columnspan=3, pady=10)

    def save_entry():
        text = message_entry.get("1.0", tk.END).strip()
        files = posting_schedule_files.copy()
        if not files:
            messagebox.showwarning("Предупреждение", 
                                   "Добавьте хотя бы один файл (фото или видео).", 
                                   parent=add_window)
            return
        if not text:
            if not messagebox.askyesno("Подтверждение", 
                                       "Сообщение пустое. Продолжить?", 
                                       parent=add_window):
                return
        photos = [f for f in files if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))]
        videos = [f for f in files if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))]
        posting_schedule.append({'text': text, 'photos': photos, 'videos': videos})
        update_posting_schedule_table()
        message_entry.delete("1.0", tk.END)
        files_listbox.delete(0, tk.END)
        posting_schedule_files.clear()
        log_queue.put("Новый набор публикации сохранен.\n")

    save_button = ttk.Button(buttons_frame, text="Сохранить", command=save_entry)
    save_button.pack(side=tk.LEFT, padx=10)

    cancel_button = ttk.Button(buttons_frame, text="Отмена", command=add_window.destroy)
    cancel_button.pack(side=tk.LEFT, padx=10)

def remove_posting_set():
    selected_items = posting_schedule_table.selection()
    if not selected_items:
        messagebox.showwarning("Предупреждение", "Выберите запись для удаления.")
        return
    try:
        indices = [int(item) for item in selected_items]
        log_queue.put(f"Выбранные индексы для удаления: {indices}\n")
        
        for index in sorted(indices, reverse=True):
            if 0 <= index < len(posting_schedule):
                del posting_schedule[index]
                log_queue.put(f"Удалена публикация №{index + 1}\n")
            else:
                log_queue.put(f"Ошибка: индекс {index} вне диапазона.\n")
        
        update_posting_schedule_table()
    except ValueError:
        messagebox.showerror("Ошибка", "Не удалось определить индексы выбранных записей.")
    except Exception as e:
        messagebox.showerror("Ошибка", f"Произошла ошибка при удалении: {e}")

def update_posting_schedule_table():
    posting_schedule_table.delete(*posting_schedule_table.get_children())
    for idx, entry in enumerate(posting_schedule, start=1):
        photos = f"{len(entry['photos'])} фото" if entry['photos'] else "0 фото"
        videos = f"{len(entry['videos'])} видео" if entry['videos'] else "0 видео"
        text_preview = (entry['text'][:30] + '...') if len(entry['text']) > 30 else entry['text']
        posting_schedule_table.insert("", "end", iid=idx-1, values=(idx, photos, videos, text_preview))

def create_posting_schedule_table():
    table_frame = ttk.Frame(content_frame)
    return table_frame

def show_table_context_menu(event, table):
    context_menu = tk.Menu(table, tearoff=0)
    context_menu.add_command(label="Выбрать все", command=lambda: select_all_table(table))
    context_menu.tk_popup(event.x_root, event.y_root)

def select_all_table(table):
    table.selection_set(table.get_children())
    return 'break'

def load_last_post():
    if os.path.exists(LAST_POST_FILE_PATH):
        try:
            with open(LAST_POST_FILE_PATH, 'r', encoding='utf-8') as file:
                value = file.read().strip()
                last_post_var.set(value)
        except Exception as e:
            log_queue.put(f"Ошибка при загрузке последнего выложенного поста: {e}\n")
    else:
        last_post_var.set("")

def save_last_post(*args):
    value = last_post_var.get()
    try:
        with open(LAST_POST_FILE_PATH, 'w', encoding='utf-8') as file:
            file.write(value)
    except Exception as e:
        log_queue.put(f"Ошибка при сохранении последнего выложенного поста: {e}\n")

# Инициализация переменных
tokens = []
successful_posts_list = []
errors_list = []
posting_schedule = []

# Создаём главное окно с поддержкой DnD
root = TkinterDnD.Tk()
root.title("Автопостинг ВКонтакте")

# Устанавливаем тему оформления (например, "clam")
style = ttk.Style()
style.theme_use("clam")

# Дополнительная настройка стилей
style.configure(".", font=FONT_STYLE)
style.configure("TButton",
                foreground="white",
                background=BUTTON_COLOR,
                padding=6,
                borderwidth=0)
style.map("TButton",
          foreground=[("disabled", "#cccccc")],
          background=[("active", "#1976D2"), ("disabled", "#90CAF9")])
style.configure("TLabel", background=SECONDARY_COLOR, foreground="black")
style.configure("TFrame", background=SECONDARY_COLOR)
style.configure("TLabelFrame", background=SECONDARY_COLOR, font=HEADER_FONT)
style.configure("Vertical.TScrollbar", troughcolor=SECONDARY_COLOR)

# Настраиваем размеры окна
window_width = 1200
window_height = 1000
root.geometry(f"{window_width}x{window_height}")

screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")
root.configure(bg=SECONDARY_COLOR)

# Переменная для управления автопрокруткой
auto_scroll_var = tk.BooleanVar(value=True)
# Переменная для галочки автопубликации
autopost_var = tk.BooleanVar(value=False)
# Переменные для лимита постов и количества фотографий
post_limit_var = tk.StringVar(value="1")
photo_count_var = tk.StringVar(value="1")

content_frame = ttk.Frame(root)
content_frame.pack(padx=20, pady=20, fill="both", expand=True)

### Прогресс-бар и статистика (верхняя часть) ###
progress_frame = ttk.Frame(content_frame)
progress_frame.pack(pady=10, fill="x")

progress_bar = ttk.Progressbar(progress_frame, length=800, mode='determinate')
progress_bar.pack(pady=(0, 5), fill="x", expand=True)

progress_label = ttk.Label(progress_frame, text="Осталось постов: 0")
progress_label.pack()

info_frame = ttk.Frame(content_frame)
info_frame.pack(pady=10, fill="x")

successful_posts_label = ttk.Label(info_frame, text="Успешные публикации: 0", foreground=PRIMARY_COLOR)
successful_posts_label.pack(side=tk.LEFT, padx=(0, 20))

failed_posts_label = ttk.Label(info_frame, text="Неуспешные публикации: 0", foreground="red")
failed_posts_label.pack(side=tk.LEFT)

### Поле "Последний выложенный пост" ###
last_post_frame = ttk.Frame(content_frame)
last_post_frame.pack(pady=10, fill="x")

last_post_label = ttk.Label(last_post_frame, text="Последний выложенный пост:")
last_post_label.pack(side=tk.LEFT, padx=(0, 10))

last_post_var = tk.StringVar()
last_post_entry = ttk.Entry(last_post_frame, textvariable=last_post_var, width=30)
last_post_entry.pack(side=tk.LEFT)
last_post_entry.config(validate="key", validatecommand=(root.register(lambda P: len(P) <= 5), '%P'))

last_post_var.trace_add('write', save_last_post)
load_last_post()

### Автопубликация и настройки ###
autopost_frame = ttk.Frame(content_frame)
autopost_frame.pack(pady=10, fill="x")

autopost_check = ttk.Checkbutton(autopost_frame, text="Автопубликация", variable=autopost_var, state=tk.DISABLED)
autopost_check.pack(side=tk.LEFT, padx=(0, 20))

post_limit_label = ttk.Label(autopost_frame, text="Лимит постов:")
post_limit_label.pack(side=tk.LEFT, padx=(0, 5))
post_limit_entry = ttk.Entry(autopost_frame, textvariable=post_limit_var, width=10)
post_limit_entry.pack(side=tk.LEFT, padx=(0, 20))

photo_count_label = ttk.Label(autopost_frame, text="Количество фотографий:")
photo_count_label.pack(side=tk.LEFT, padx=(0, 5))
photo_count_entry = ttk.Entry(autopost_frame, textvariable=photo_count_var, width=10)
photo_count_entry.pack(side=tk.LEFT)

# Отслеживаем изменения состояния галочки автопубликации
autopost_var.trace_add('write', update_add_schedule_button_state)

### Кнопки "Указать аккаунты" и др. ###
account_frame = ttk.Frame(content_frame)
account_frame.pack(pady=10, fill="x")

account_button = ttk.Button(account_frame, text="Указать аккаунты", command=load_tokens)
account_button.pack(side=tk.LEFT, padx=(0, 10))

tokens_label = ttk.Label(account_frame, text="Токенов: 0")
tokens_label.pack(side=tk.LEFT)

# Отключаем главный DnD (пустая функция)
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', lambda event: None)

### Основные кнопки управления ###
controls_frame = ttk.Frame(content_frame)
controls_frame.pack(pady=10)

post_button = ttk.Button(controls_frame, text="Выложить пост", command=submit_post, state=tk.DISABLED)
post_button.pack(side=tk.LEFT, padx=(0, 10))

stop_button = ttk.Button(controls_frame, text="Остановить", command=stop_posting, state=tk.DISABLED)
stop_button.pack(side=tk.LEFT, padx=(0, 10))

add_schedule_button = ttk.Button(controls_frame, text="Добавить набор публикаций", command=add_posting_set, state=tk.DISABLED)
add_schedule_button.pack(side=tk.LEFT, padx=(0, 10))

remove_schedule_button = ttk.Button(controls_frame, text="Удалить выбранные", command=remove_posting_set, state=tk.DISABLED)
remove_schedule_button.pack(side=tk.LEFT, padx=(0, 10))

### Область расписания и логов ###
tables_frame = ttk.Frame(content_frame)
tables_frame.pack(pady=10, fill="both", expand=True)

# Левая часть — таблица расписания публикаций
left_table_frame = ttk.Frame(tables_frame)
left_table_frame.pack(side=tk.LEFT, fill="both", expand=True, padx=(0, 10))

posting_schedule_table = ttk.Treeview(left_table_frame, 
                                      columns=("№", "Фото", "Видео", "Текст"), 
                                      show='headings', 
                                      selectmode='extended', 
                                      height=10)
for col in ("№", "Фото", "Видео", "Текст"):
    posting_schedule_table.heading(col, text=col)
    if col == "Текст":
        posting_schedule_table.column(col, width=300)
    else:
        posting_schedule_table.column(col, width=100)
posting_schedule_table.pack(side=tk.LEFT, fill="both", expand=True)

scrollbar_table = ttk.Scrollbar(left_table_frame, orient="vertical", command=posting_schedule_table.yview)
scrollbar_table.pack(side=tk.RIGHT, fill="y")
posting_schedule_table.config(yscrollcommand=scrollbar_table.set)

posting_schedule_table.bind("<Button-3>", lambda event: show_table_context_menu(event, posting_schedule_table))
posting_schedule_table.bind("<Control-a>", lambda event: select_all_table(posting_schedule_table))
posting_schedule_table.bind("<Control-A>", lambda event: select_all_table(posting_schedule_table))

# Правая часть — логи процесса
right_log_frame = ttk.Frame(tables_frame)
right_log_frame.pack(side=tk.LEFT, fill="both", expand=True, padx=(10, 0))

output_label = ttk.Label(right_log_frame, text="Процесс публикации:")
output_label.pack(anchor="w", pady=(0, 5))

output_frame = ttk.Frame(right_log_frame)
output_frame.pack(pady=(0, 10), fill="both", expand=True)

output_text = tk.Text(output_frame, 
                      width=50, 
                      height=10, 
                      font=FONT_STYLE, 
                      wrap="word", 
                      bg="#f9f9f9", 
                      relief="solid", 
                      bd=1)
output_text.pack(side=tk.LEFT, fill="both", expand=True)

output_scrollbar = ttk.Scrollbar(output_frame, orient="vertical", command=output_text.yview)
output_scrollbar.pack(side=tk.RIGHT, fill="y")
output_text.config(yscrollcommand=output_scrollbar.set)
output_text.bind("<Button-3>", lambda event: show_context_menu(event, output_text))

### Область успешных и неуспешных публикаций ###
publishing_frame = ttk.Frame(content_frame)
publishing_frame.pack(pady=10, fill="both", expand=True)

successful_frame = ttk.LabelFrame(publishing_frame, text="Успешные публикации", padding=5)
successful_frame.pack(side=tk.LEFT, padx=10, pady=10, fill="both", expand=True)

successful_text_frame = ttk.Frame(successful_frame)
successful_text_frame.pack(fill="both", expand=True)

successful_text = tk.Text(successful_text_frame, 
                          width=40, 
                          height=10, 
                          font=FONT_STYLE, 
                          cursor="hand2", 
                          wrap="word", 
                          bg="#e8f5e9", 
                          relief="solid", 
                          bd=1)
successful_text.pack(side=tk.LEFT, fill="both", expand=True)

successful_scrollbar = ttk.Scrollbar(successful_text_frame, orient="vertical", command=successful_text.yview)
successful_scrollbar.pack(side=tk.RIGHT, fill="y")
successful_text.config(yscrollcommand=successful_scrollbar.set)
successful_text.bind("<Button-3>", lambda event: show_context_menu(event, successful_text))

failed_frame = ttk.LabelFrame(publishing_frame, text="Неуспешные публикации", padding=5)
failed_frame.pack(side=tk.RIGHT, padx=10, pady=10, fill="both", expand=True)

failed_text_frame = ttk.Frame(failed_frame)
failed_text_frame.pack(fill="both", expand=True)

failed_text = tk.Text(failed_text_frame, 
                      width=40, 
                      height=10, 
                      font=FONT_STYLE, 
                      wrap="word", 
                      bg="#ffebee", 
                      relief="solid", 
                      bd=1)
failed_text.pack(side=tk.LEFT, fill="both", expand=True)

failed_scrollbar = ttk.Scrollbar(failed_text_frame, orient="vertical", command=failed_text.yview)
failed_scrollbar.pack(side=tk.RIGHT, fill="y")
failed_text.config(yscrollcommand=failed_scrollbar.set)
failed_text.bind("<Button-3>", lambda event: show_context_menu(event, failed_text))

root.after(100, process_queue)
root.mainloop()